package com.ganbing518.commons.utils;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.ganbing518.commons.factory.NamedThreadFactory;
import org.apache.http.*;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.client.utils.HttpClientUtils;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.LaxRedirectStrategy;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StopWatch;

import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.Serializable;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.util.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * Description:
 *
 * @author gan bing
 * @version V1.0
 * @date 2018-07-16 16:40
 */
public class HttpClientUtil {

    private static final Logger log = LoggerFactory.getLogger(HttpClientUtil.class);

    private static PoolingHttpClientConnectionManager connMgr;
    private static CloseableHttpClient httpClient;
    private static RequestConfig requestConfig;

    private static final int RETRY_TIMES = 3;

    static {

        NamedThreadFactory threadFactory = new NamedThreadFactory("HttpClientUtil-connMgr-",true);

        ScheduledExecutorService scheduleds = Executors.newScheduledThreadPool(3, threadFactory);

        HttpRequestRetryHandler httpRequestRetryHandler = new HttpRequestRetryHandler() {
            @Override
            public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
                if (executionCount >= RETRY_TIMES) {
                    return false;
                }
                if (exception instanceof NoHttpResponseException) {
                    return true;
                }
                if (exception instanceof SSLHandshakeException) {
                    return false;
                }
                if (exception instanceof ConnectTimeoutException) {
                    return false;
                }
                if (exception instanceof InterruptedIOException) {
                    return false;
                }
                if (exception instanceof UnknownHostException) {
                    return false;
                }
                if (exception instanceof SSLException) {
                    return false;
                }

                HttpClientContext clientContext = HttpClientContext.adapt(context);
                HttpRequest request = clientContext.getRequest();
                return !(request instanceof HttpEntityEnclosingRequest);
            }
        };

        //设置连接池
        connMgr = new PoolingHttpClientConnectionManager();
        connMgr.setMaxTotal(200);
        connMgr.setDefaultMaxPerRoute(100);
        //设置超时时间
        RequestConfig.Builder configBuilder = RequestConfig.custom();
        configBuilder.setSocketTimeout(30000);
        configBuilder.setConnectTimeout(30000);
        configBuilder.setConnectionRequestTimeout(30000);
        requestConfig = configBuilder.build();
        httpClient = org.apache.http.impl.client.HttpClients.custom()
                .setConnectionManager(connMgr)
                .setDefaultRequestConfig(requestConfig)
                .setRedirectStrategy(new LaxRedirectStrategy())
                .setRetryHandler(httpRequestRetryHandler)
                .build();


        scheduleds.scheduleWithFixedDelay(() -> {
            try {
                // 关闭失效的连接
                connMgr.closeExpiredConnections();
                // 可选的, 关闭30秒内不活动的连接
                connMgr.closeIdleConnections(30, TimeUnit.SECONDS);
            } catch (Throwable e) {
                log.error("HttpClientUtil-connMgr, errorMsg:{}", e.getMessage(), e);
            }
        }, 30000, 3000, TimeUnit.MILLISECONDS);

        scheduleds.scheduleWithFixedDelay(() -> {

            try {
                Set<HttpRoute> routes = connMgr.getRoutes();
                if (CollectionUtils.isEmpty(routes)) {
                    log.info("HttpClientUtil-connMgr-total:{}", connMgr.getTotalStats().toString());
                } else {
                    Map<String, String> routeMap = connMgr.getRoutes().stream().collect(Collectors.toMap(k -> k.getTargetHost().toHostString(), v -> connMgr.getStats(v).toString()));
                    log.info("HttpClientUtil-connMgr-total:{}, routes:{}", connMgr.getTotalStats().toString(), JSON.toJSONString(routeMap));
                }
            } catch (Throwable e) {
                log.error("HttpClientUtil-connMgr, errorMsg:{}", e.getMessage(), e);
            }

        }, 10000, 10000, TimeUnit.MILLISECONDS);

    }

    public static <T extends Serializable> Result<T> get(String url, Charset charset) throws Exception {
        log.info("调用接口:{}", url);

        Result<T> result = new Result<>();
        CloseableHttpResponse response = null;
        HttpGet request;
        try {
            StopWatch sw = new StopWatch();
            sw.start();

            request = new HttpGet(url);


            request.setConfig(requestConfig);
            response = httpClient.execute(request);
            result.setStatusCode(response.getStatusLine().getStatusCode());
            if (result.getStatusCode() == HttpStatus.SC_OK) {
                result.setContent(getContent(response.getEntity(), charset));
            }

            sw.stop();
            log.info("调用接口:{}, 执行结果:{}, 耗时:{} ms", url, result, sw.getTotalTimeMillis());
        } catch (Exception e) {
            log.error("调用接口:{}, errorMsg:{}", url, e.getMessage(), e);
            throw e;
        } finally {
            HttpClientUtils.closeQuietly(response);
        }
        return result;
    }

    public static <T extends Serializable>  Result<T> post(String url, Map<String, String> params, Charset charset) throws Exception {
        log.info("调用接口:{}, 参数:{}", url, JSON.toJSONString(params));

        Result<T> result = new Result<>();
        CloseableHttpResponse response = null;
        HttpPost request;
        try {
            StopWatch sw = new StopWatch();
            sw.start();

            request = new HttpPost(url);


            request.setConfig(requestConfig);

            if (!CollectionUtils.isEmpty(params)) {
                List<NameValuePair> paramList = new ArrayList<>();
                for (Map.Entry<String, String> param : params.entrySet()) {
                    paramList.add(new BasicNameValuePair(param.getKey(), param.getValue()));
                }
                request.setEntity(new UrlEncodedFormEntity(paramList, charset));
            }

            response = httpClient.execute(request);
            result.setStatusCode(response.getStatusLine().getStatusCode());
            if (result.getStatusCode() == HttpStatus.SC_OK) {
                result.setContent(getContent(response.getEntity(), charset));
            }

            sw.stop();
            log.info("调用接口:{}, 执行结果:{}, 耗时:{} ms", url, result, sw.getTotalTimeMillis());
        } catch (Exception e) {
            log.error("调用接口:{}, 参数:{}, errorMsg:{}", url, JSON.toJSONString(params), e.getMessage(), e);
            throw e;
        } finally {
            HttpClientUtils.closeQuietly(response);
        }
        return result;
    }

    public static <T extends Serializable>  Result<T> json(String url, String param, Charset charset) throws Exception {
        log.info("调用接口:{}, 参数:{}", url, param);
        Result<T> result = new Result<>();
        CloseableHttpResponse response = null;
        HttpPost request;
        try {
            StopWatch sw = new StopWatch();
            sw.start();

            request = new HttpPost(url);


            request.setHeader("Content-Type", "application/json;charset=UTF-8");
            request.setConfig(requestConfig);
            StringEntity entity = new StringEntity(param, charset);
            entity.setContentEncoding("UTF-8");
            entity.setContentType("application/json");
            request.setEntity(entity);
            response = httpClient.execute(request);
            result.setStatusCode(response.getStatusLine().getStatusCode());
            if (result.getStatusCode() == HttpStatus.SC_OK) {
                result.setContent(getContent(response.getEntity(), charset));
            }

            sw.stop();
            log.info("调用接口:{}, 执行结果:{}, 耗时:{} ms", url, result, sw.getTotalTimeMillis());
        } catch (Exception e) {
            log.error("调用接口:{}, 参数:{}, errorMsg:{}", url, param, e.getMessage(), e);
            throw e;
        } finally {
            HttpClientUtils.closeQuietly(response);
        }
        return result;

    }

    public static <T extends Serializable> Result<T> customJson(String url, String param, Charset charset, Map<String, Object> header, RequestConfig config) throws Exception {
        log.info("调用接口:{}, 参数:{}", url, param);

        Result<T> result = new Result<>();
        CloseableHttpResponse response = null;
        HttpPost request;
        try {
            StopWatch sw = new StopWatch();
            sw.start();

            request = new HttpPost(url);

            if (Objects.isNull(config)) {
                request.setConfig(requestConfig);
            } else {
                request.setConfig(config);
            }

            if (header != null) {
                for (Map.Entry<String, Object> entry : header.entrySet()) {
                    String value = entry.getValue() == null ? null : entry.getValue().toString();
                    request.addHeader(entry.getKey(), value);
                }
            }

            request.setHeader("Content-Type", "application/json;charset=" + charset.name());
            if (param != null && param.length() > 0) {
                StringEntity entity = new StringEntity(param, charset);// 解决中文乱码问题
                entity.setContentEncoding(charset.name());
                entity.setContentType("application/json");
                request.setEntity(entity);
            }

            response = httpClient.execute(request);
            result.setStatusCode(response.getStatusLine().getStatusCode());
            if (result.getStatusCode() == HttpStatus.SC_OK) {
                result.setContent(getContent(response.getEntity(), charset));
            }

            sw.stop();
            log.info("调用接口:{}, 执行结果:{}, 耗时:{} ms", url, result, sw.getTotalTimeMillis());
        } catch (Exception e) {
            log.error("调用接口:{}, 参数:{}, errorMsg:{}", url, param, e.getMessage(), e);
            throw e;
        } finally {
            HttpClientUtils.closeQuietly(response);
        }
        return result;
    }

    private static <T extends Serializable> T getContent(HttpEntity httpEntity, Charset charset) throws IOException {
        if (httpEntity != null) {
            return JSON.parseObject(EntityUtils.toString(httpEntity, charset),new TypeReference<T>(){});
        }
        return null;
    }


    public static class Result<T extends Serializable> implements Serializable {

        private static final long serialVersionUID = 1L;

        private int statusCode;

        private T content;

        public int getStatusCode() {
            return statusCode;
        }

        public void setStatusCode(int statusCode) {
            this.statusCode = statusCode;
        }

        public T getContent() {
            return content;
        }

        public void setContent(T content) {
            this.content = content;
        }

        @Override
        public String toString() {
            return "{ statusCode:" + statusCode + ",content" + content.toString() + "}";
        }
    }
}
